<?php
/**
 * Copyright 2011  SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */
?>
<?php
/**
 * RDF document
 * 
 * Represents an RDF document, containing zero or more RDF resources.
 */
class simplerdf_RdfDocument extends simplerdf_Rdf
{
	protected $schema = null;
	protected $resources = array();
	
	/**
	 * Construct a new simplerdf_RdfDocument.
	 * @param string|array $rdfXmlOrSimpleIndex Either a string containing a RDF/XML document or an ARC2 SimpleIndex
	 * @param string $base XML base URL (optional)
	 * @param simplerdf_RdfDocument $schema RDF Schema (optional)
	 */
	public function __construct($rdfXmlOrSimpleIndex, $base = '', &$schema = null)
	{
		$this->schema =& $schema;

		if(!is_array($rdfXmlOrSimpleIndex))
		{
			$parser = ARC2::getRDFXMLParser();
			$parser->parse($base, $rdfXmlOrSimpleIndex);
			//TODO: error handling
			$rdfXmlOrSimpleIndex = $parser->getSimpleIndex();
		}
		
		foreach($rdfXmlOrSimpleIndex as $subject => $resourceIndex)
		{
			$this->resources[$subject] = new simplerdf_RdfResource(array($subject => $resourceIndex));
		}
	}
	
	/**
	 * Load an RDF document and merge it into the current one.
	 * @param string|array $rdfXmlOrSimpleIndex Either a string containing a RDF/XML document or an ARC2 SimpleIndex
	 * @param string $base XML base URL (optional)
	 */
	public function load($rdfXmlOrSimpleIndex, $base = '')
	{
		// parse the RDF data
		$rdfDocument = new self($rdfXmlOrSimpleIndex, $base);
		
		// merge it into the current document
		$rdfResources = $rdfDocument->getResources();
		foreach($rdfResources as $rdfResource)
		{
			$this->addResource($rdfResource);
		}
	}
	
	/**
	 * Create a new (empty) document.
	 * @param string $subject The subject of the resource 
	 * @param simplerdf_RdfDocument $schema RDF Schema (optional)
	 * @return simplerdf_RdfDocument The new (empty) resource
	 */
	public static function createRdfDocument(&$schema = null)
	{
		$index = array();
		
		return new simplerdf_RdfDocument($index, '', $schema);
	}
	
	/**
	 * Adds a resource to this document. Existing resources will be merged. 
	 * @param simplerdf_RdfResource $rdfResource RdfResource to add to this document
	 * @return simplerdf_RdfResource RdfResource
	 */
	public function &addResource($rdfResource)
	{
		$subject = $rdfResource->getSubject();
		if(!isset($this->resources[$subject]))
		{
			// no resource with the given subject exists, add the RdfResource as a whole
			$this->resources[$subject] = $rdfResource;
		}
		else
		{
			// a resource with the given subject already exists, merge the RdfResource into it
			$existingRdfResource =& $this->resources[$subject];
			
			$predicates = $rdfResource->getPredicates();
			foreach($predicates as $predicate)
			{
				$existingRdfResource->addStatement($predicate, $rdfResource->getObjects($predicate));
			}
		}
		
		return $this->resources[$subject];
	}
	
	/**
	 * Adds a new empty resource to this document. Existing resources will not be overwritten. 
	 * @param string $subject Subject URI of the resource, if null a blank node ID will be generated
	 * @return simplerdf_RdfResource RdfResource
	 */
	public function &createResource($subject = null)
	{
		if($subject === null)
		{
			$subject = $this->generateBlankNodeId();
		}
		
		if(!isset($this->resources[$subject]))
		{
			$rdfResource = simplerdf_RdfResource::createRdfResource($subject, $this->schema);
			$this->resources[$subject] =& $rdfResource;
		}
		else
		{
			$rdfResource =& $this->resources[$subject];
		}
		
		return $rdfResource;
	}
	
	/**
	 * Get the number of resources in this document.
	 * @return int Number of resources in this document
	 */
	public function getResourceCount()
	{
		return count($this->resources);
	}
		
	/**
	 * Get a resource from this document specified with a numerical index.
	 * @return simplerdf_RdfResource RdfResource
	 */
	public function &getResource($idx)
	{
		$subjects = array_keys($this->resources);
		if(isset($this->resources[$subjects[$idx]]))
		{
			return $this->resources[$subjects[$idx]];
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * Get a resource from this document specified with the subject.
	 * @return simplerdf_RdfResource RdfResource
	 */
	public function &getResourceBySubject($subject)
	{
		if(isset($this->resources[$subject]))
		{
			return $this->resources[$subject];
		}
		else
		{
			return null;
		}
	}
	
	/**
	 * Get all resources for this document.
	 * @return array Array containing RdfResource objects
	 */
	public function &getResources()
	{
		return $this->resources;
	}

	/**
	 * Creates a list of subjects and objects matching the specified predicate.
	 * @param string $predicate The predicate to match
	 * @param string $subject The subject to match
	 * @return array If subject is provided, an array of objects, otherwise, an array, keys contain subjects, values contain arrays with objects
	 */
	public function getSubjectObjectsList($predicate, $subject = null)
	{
		//TODO: cache?
		$subjectObjectsList = array();

		if($subject === null)
		{
			// loop over all resources
			foreach($this->resources as $resource)
			{
				if($resource !== null)
				{
					// find objects matching specified predicate
					$objects = $resource->getObjects($predicate);
					if(count($objects) > 0)
					{
						// check if a sub array has already been created for this subject
						if(!isset($subjectObjectsList[$resource->getSubject()]))
						{
							$subjectObjectsList[$resource->getSubject()] = array();
						}
						
						// add the object to the subject array
						foreach($objects as $object)
						{
							$subjectObjectsList[$resource->getSubject()][] = $object;
						}
					}
				}
			}
		}
		else
		{
			$rdfResource = $this->getResourceBySubject($subject);
			if($rdfResource !== null)
			{
				$subjectObjectsList = $rdfResource->getObjects($predicate);
			}
		}
		
		return $subjectObjectsList;
	}
	
	/**
	 * Creates a list of objects and subjects matching the specified predicate.
	 * @param string $predicate The predicate to match
	 * @param string $object The subject to match
	 * @return array If object is provided, an array of subjects, otherwise, an array, keys contain objects, values contain arrays with subjects
	 */
	public function getObjectSubjectsList($predicate, $object = null)
	{
		//TODO: cache?
		$objectSubjectsList = array();

		// loop over all resources
		foreach($this->resources as $resource)
		{
			// find objects matching specified predicate
			$resourceObjects = $resource->getObjects($predicate);
			if(count($resourceObjects) > 0)
			{
				// add the subjects to the object array
				foreach($resourceObjects as $resourceObject)
				{
					if($object !== null)
					{
						if($resourceObject === $object)
						{
							$objectSubjectsList[] = $resource->getSubject();
						}
					}
					else
					{
						$objectSubjectsList[$resourceObject][] = $resource->getSubject();
					}
				}
			}
		}
		
		return $objectSubjectsList;
	}
	
	protected function &buildTreeList(&$array, $key, &$acceptConditions = null, &$rejectConditions = null, &$list = array())
	{
		@$values = $array[$key];
		if(isset($values))
		{
			foreach($values as $value)
			{
				$accept = false;
				$resource = null;
				if($acceptConditions !== null || $rejectConditions !== null)
				{
					$resource = $this->getResourceBySubject($value);
				}
				if($acceptConditions !== null)
				{
					foreach($acceptConditions as $predicate => $acceptedValue)
					{
						if(!$resource->hasStatement($predicate, $acceptedValue))
						{
							$accept = false;
							break;
						}
					}
				}
				else
				{
					$accept = true;
				}
				if($rejectConditions !== null)
				{
					foreach($rejectConditions as $predicate => $rejectValue)
					{
						if($resource->hasStatement($predicate, $rejectValue))
						{
							$accept = false;
							break;
						}
					}
				}
				if(preg_match('/^_:/', $value))
				{
					$accept = false;
				}
				
				if($accept)
				{
					if(!in_array($value, $list))
					{
						$list[] = $value;
						$this->buildTreeList($array, $value, $acceptConditions, $rejectConditions, $list);
					}
				}
			}
		}
		return $list;
	}
	
	protected function &buildTree(&$array, $key, &$acceptConditions = null, &$rejectConditions = null)
	{
		@$values = $array[$key];
		if(isset($values))
		{
			foreach($values as $value)
			{
				$accept = false;
				$resource = null;
				if($acceptConditions !== null || $rejectConditions !== null)
				{
					$resource = $this->getResourceBySubject($value);
				}
				if($acceptConditions !== null)
				{
					foreach($acceptConditions as $predicate => $acceptedValue)
					{
						if(!$resource->hasStatement($predicate, $acceptedValue))
						{
							$accept = false;
							break;
						}
					}
				}
				else
				{
					$accept = true;
				}
				if($rejectConditions !== null)
				{
					foreach($rejectConditions as $predicate => $rejectValue)
					{
						if($resource->hasStatement($predicate, $rejectValue))
						{
							$accept = false;
							break;
						}
					}
				}
				if(preg_match('/^_:/', $value))
				{
					$accept = false;
				}
				
				if($accept)
				{
					$subTree = $this->buildTree($array, $value, $acceptConditions, $rejectConditions);
					$tree[$value] = $subTree;
				}
			}
		}
		
		return $tree;
	}
	
	/**
	 * Creates a list of subjects and objects matching the specified predicate, following hierarchy chains.
	 * For example:
	 * B isSubPropertyOf A
	 * C isSubPropertyOf B
	 * D isSubPropertyOf B
	 * Will result in:
	 * B => [A]
	 * C => [B,A]
	 * D => [B,A]
	 * @param string $predicate The predicate to match
	 * @param string $subject If a subject URI is provided, only the list for the subject specified will be returned  
	 * @return array If a subject URI is provided: an arrays with objects, otherwise: an array, keys contain subjects, values contain arrays with objects  
	 */
	public function getChainedSubjectObjectsList($predicate, $subject = null)
	{
		$chainedSubjectObjectsList = array();
		
		$subjectObjectsList = $this->getSubjectObjectsList($predicate);
		
		if($subject === null)
		{
			foreach($subjectObjectsList as $subject => $objects)
			{
				$chainedSubjectObjectsList[$subject] = $this->buildTreeList($subjectObjectsList, $subject);
			}
		}
		else
		{
			$chainedSubjectObjectsList = $this->buildTreeList($subjectObjectsList, $subject);
		}
		
		return $chainedSubjectObjectsList;
	}
	
	/**
	 * Creates a list of objects and subjects matching the specified predicate, following hierarchy chains.
	 * For example:
	 * B isSubPropertyOf A
	 * C isSubPropertyOf B
	 * D isSubPropertyOf B
	 * Will result in:
	 * A => [B,C,D]
	 * B => [C,D]
	 * @param string $predicate The predicate to match
	 * @param string $object If a object URI is provided, only the list for the object specified will be returned  
	 * @return array If an object URI is provided: an arrays with subjects, otherwise: an array, keys contain objects, values contain arrays with subjects  
	 */
	public function getChainedObjectSubjectsList($predicate, $object = null)
	{
		$chainedObjectSubjectsList = array();

		$objectSubjectsList = $this->getObjectSubjectsList($predicate);
		
		if($object === null)
		{
			foreach($objectSubjectsList as $object => $subject)
			{
				$chainedObjectSubjectsList[$object] = $this->buildTreeList($objectSubjectsList, $object);
			}
		}
		else
		{
			$chainedObjectSubjectsList = $this->buildTreeList($objectSubjectsList, $object);
		}
		
		return $chainedObjectSubjectsList;
	}

	/**
	 * Creates a tree of subjects and objects matching the specified predicate, following hierarchy chains.
	 * @param string $predicate The predicate to match
	 * @param string $subject If a subject URI is provided, only the tree for the subject specified will be returned
	 * @return array Nested array tree structure
	 */
	public function getChainedSubjectObjectsTree($predicate, $subject = null)
	{
		$hierarchicalSubjectObjectsList = array();
		
		$subjectObjectsList = $this->getSubjectObjectsList($predicate);

		if($subject === null)
		{
			foreach($subjectObjectsList as $subject => $objects)
			{
				$hierarchicalSubjectObjectsList[$subject] = $this->buildTree($subjectObjectsList, $subject);
			}
		}
		else
		{
			$hierarchicalSubjectObjectsList = $this->buildTree($subjectObjectsList, $subject);
		}
		
		return $hierarchicalSubjectObjectsList;
	}
	
	/**
	 * Creates a tree of objects and subjects matching the specified predicate, following hierarchy chains.
	 * @param string $predicate The predicate to match
	 * @param string $object If an object URI is provided, only the tree for the object specified will be returned
	 * @return array Nested array tree structure
	 */
	public function getChainedObjectSubjectsTree($predicate, $object = null, $acceptConditions = null, $rejectConditions = null)
	{
		$hierarchicalObjectSubjectsList = array();
		
		$objectSubjectsList = $this->getObjectSubjectsList($predicate);
		
		if($object === null)
		{
			foreach($objectSubjectsList as $object => $subject)
			{
				$hierarchicalObjectSubjectsList[$object] = $this->buildTree($objectSubjectsList, $object, $acceptConditions, $rejectConditions);
			}
		}
		else
		{
			$hierarchicalObjectSubjectsList = $this->buildTree($objectSubjectsList, $object, $acceptConditions, $rejectConditions);
		}
		
		return $hierarchicalObjectSubjectsList;
	}

	/**
	 * Get a RDF collection
	 * @param $collectionItemUri URI of the RDF collection 
	 * @return array An array of items in the collection 
	 */
	public function getRdfCollection($collectionItemUri)
	{
		$listItems = array();
		while($collectionItemUri !== null && $collectionItemUri !== 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil')
		{
			$collectionItem = $this->getResourceBySubject($collectionItemUri);
			$listItem = $collectionItem->getFirstObject('http://www.w3.org/1999/02/22-rdf-syntax-ns#first');
			if($listItem !== null)
			{
				$listItems[] = $listItem;
			}
			$collectionItemUri = $collectionItem->getFirstObject('http://www.w3.org/1999/02/22-rdf-syntax-ns#rest');
		}
		
		return $listItems;
	}
	
	/**
	 * Return the current document expressed in RDF/XML.
	 * @param bool $skipXmlDeclaration If true, the <?xml version="1.0" encoding="UTF-8"?> section is not returned
	 * @return string The current document expressed in RDF/XML
	 */
	public function getRdfXml($skipXmlDeclaration = false)
	{
		
		$ser = $this->getRdfXmlSerializer();
		// use a simple index with flattened objects, the RDF/XML serializer performs automatic type guessing on flattened indexes
		$rdfXml = $ser->getSerializedIndex($this->_getSimpleIndex());
		if($skipXmlDeclaration)
		{
			$count = 1;
			$rdfXml = str_replace("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "", $rdfXml, $count);
		}
		return $rdfXml;
	}
	
	/**
	 * Return the current document expressed in RDF/JSON.
	 * @return string The current document expressed in RDF/JSON
	 */
	public function getRdfJson()
	{
		
		$ser = $this->getRdfJsonSerializer();
		// use a typed simple index, the RDF/JSON serializer does not perform type guessing on flattened indexes
		$rdfJson = $ser->getSerializedIndex($this->_getSimpleIndex(false));
		return $rdfJson;
	}
	
	/**
	 * Return the current resource expressed as an ARC2 SimpleIndex structure.
	 * @param bool $flattenObjects If true, no object type information is provided
	 * @return array ARC2 SimpleIndex
	 */
	public function _getSimpleIndex($flattenObjects = true)
	{
		$index = array();
		foreach($this->resources as $rdfResource)
		{
			$resourceIndex =& $rdfResource->_getSimpleIndex($flattenObjects);
			$index[$rdfResource->getSubject()] =& $resourceIndex[$rdfResource->getSubject()];
		}
		
		return $index;
	}
	
	/**
	 * Generate a random blank node ID.
	 * @return string random blank node ID
	 */
	protected function generateBlankNodeId()
	{
		return '_:node' . dechex(mt_rand());
	}
}
